import os
import re
import subprocess

import bpy
from bpy.props import StringProperty, BoolProperty, FloatVectorProperty, FloatProperty, EnumProperty, IntProperty
from bpy_types import PropertyGroup

from . import QueueUtils, from_file_get_file_size, _byte_to_mb, _valid_path
from .log import log


def from_path(property_path: str) -> 'bpy.types.Property':
    """从输入的文本路径获取blender属性"""
    import bpy
    sp_prop = property_path.split('.')
    prefix = sp_prop[0]
    if prefix == 'bpy':
        prop = bpy
    elif prefix == 'C':
        prop = bpy.context
    else:
        log.info(f'property_path: {property_path} 无效')
        return None
    try:
        for pr in sp_prop[1:]:
            prop = getattr(prop, pr, None)
        return prop
    except AttributeError:
        log.error(f'{property_path} api路径错误,未找到', {prop})


class RenderProperty(PropertyGroup, QueueUtils):
    render_state_items = QueueUtils.render_state_items

    def update_file(self, context):
        self.clear_cache()
        self.name = self.asset_name

    render_blend_file: StringProperty(name='需要渲染的文件',
                                      subtype='FILE_PATH',
                                      update=update_file,
                                      )

    def update_selected(self, context):
        """
        shift 全部同步到此项
        """
        if self.pref.is_update:
            return

    selected: BoolProperty(name='已选中',
                           default=False,
                           update=update_selected,
                           )

    # Color
    render_state_color = {
        'NOT_RENDER': [0.1, 0.1, 0.1, 1.],
        'IN_QUEUE': [0.1, 0.3, 0.1, 1.],
        'IN_RENDER': [0.1, 0.7, 0.1, 1.],
        'PAUSE_RENDER': [1, 0.04, 0.0, 1],

        'RENDERED': [0.5, 0.5, 0.5, 1.],

        'RENDER_ERROR': [0.1, 0.1, 0.5, 1.],
        'FILE_ERROR': [0.5, 0.1, 0.5, 1.],
    }

    def get_color(self):
        """通过self.render_state_items 获取对应的颜色
        """
        if self.render_state in self.render_state_color:
            return self.render_state_color[self.render_state]
        return [1, 1, 1, 1]

    def set_color(self, value):
        ...

    state_color: FloatVectorProperty(name='状态颜色',
                                     description='''黑色表示代渲染\n亮绿色表示渲染中\n暗绿色表示渲染队列中\n红色表示渲染错误\n紫色表示文件错误\n''',
                                     size=4,
                                     subtype='COLOR',
                                     get=get_color,
                                     set=set_color,
                                     )

    def get_size(self):
        """获取资产的大小并反回mb单位的"""
        if 'blend_size' in self:
            size = self['blend_size']
        elif self.render_blend_file:
            size = self['blend_size'] = from_file_get_file_size(
                self.render_blend_file)
        else:
            size = -1
        e = _byte_to_mb(size)

        return round(e, 2)

    def set_size(self, value):
        self['blend_size'] = value

    blend_size: FloatProperty(name='渲染文件大小,用于预估渲染时间',
                              get=get_size,
                              set=set_size,
                              )

    # state
    def update_state(self, context):
        """更新渲染状态"""
        if self.is_in_queue:
            # 如果添加到队列了,则添加到self.render_queue
            self.render_queue.append(self)
        elif self.is_not_render and self in self.render_queue:
            # 如果设置成了待渲染并且此项在渲染队列里面,就删掉它
            self.render_queue.remove(self)

    _render_state_switch = {
        'NOT_RENDER': 'IN_QUEUE',  # 添加到队列
        'IN_QUEUE': 'NOT_RENDER',  # 不渲染
        'IN_RENDER': 'NOT_RENDER',  # 不渲染
        'RENDER_ERROR': 'IN_QUEUE',  # 重试

        'RENDERED': 'NOT_RENDER',  # 渲染完成项可以重新渲
    }

    def switch_state(self):
        """切换渲染状态"""
        state = self.render_state
        if state == 'IN_RENDER':
            self.stop_render()

        if state in self._render_state_switch:
            if self.is_in_render:
                self.stop_render()
            self.render_state = self._render_state_switch[state]

    render_state: EnumProperty(
        items=render_state_items,
        default='NOT_RENDER',
        update=update_state,
    )

    rendered_image_file_path: StringProperty(name='渲染文件路径', subtype='FILE_PATH')

    render_error_count: IntProperty(name='渲染错误次数', description='用于确认渲染')


class GetProperty(RenderProperty):

    @property
    def all_selected(self):
        """反回所有选中渲染项
        return list[RenderItem]"""
        return [item for item in self.pref.render_items if item.selected]

    @property
    def is_not_render(self) -> bool:
        return self.render_state == 'NOT_RENDER'

    @property
    def is_in_queue(self) -> bool:
        return self.render_state == 'IN_QUEUE'

    @property
    def is_in_render(self) -> bool:
        return self.render_state == 'IN_RENDER'

    @property
    def is_pause_render(self) -> bool:
        return self.render_state == 'PAUSE_RENDER'

    @property
    def is_rendered(self) -> bool:
        return self.render_state == 'RENDERED'

    @property
    def is_render_error(self) -> bool:
        return self.render_state == 'RENDER_ERROR'

    @property
    def is_file_error(self) -> bool:
        return self.render_state == 'FILE_ERROR'

    @property
    def check_file(self) -> bool:
        """检查此渲染文件是否存在"""
        return os.path.isfile(self.render_blend_file)

    @property
    def asset_name(self) -> str:
        """通过资产显示"""
        path = self.render_blend_file
        return os.path.splitext(os.path.split(path)[1])[0]

    @property
    def valid(self) -> bool:
        """反回此渲染项是否有效的布尔值"""
        return _valid_path(self.render_blend_file)

    # Time

    def get_render_progress(self) -> int:
        """获取渲染进度
        通过平铺渲染数和采样数得出进度
            """
        _tiles = re.compile(r'Rendered +(.+) Tiles+', re.S)
        _simple = re.compile(r' Sample +(.+)', re.S)
        last = self.read_render_progress
        if not last:
            return -1
        tiles = re.findall(_tiles, last)
        simple = re.findall(_simple, last)

        if not simple:
            return -1

        render_simple = sum_simple = 0  # 已渲染采样/总采样
        a_s, b_s = simple[0].split('/')  # 已采样/总采样

        if tiles:
            a_t, b_t = tiles[0].split('/')
            render_simple += int(a_t) * int(b_s)
            sum_simple += int(b_s) * int(b_t)
        else:
            # 不用平铺
            sum_simple += int(b_s)

        render_simple += int(a_s)

        return int((render_simple / sum_simple) * 100)

    def set_prop(self, value):
        """占位用设置属性方法"""

    render_progress: IntProperty(name='渲染进度',
                                 min=-1,
                                 max=100,
                                 get=get_render_progress,
                                 set=set_prop,
                                 subtype='PERCENTAGE',
                                 )

    def get_sum_progress(self):
        """获取队列渲染的进度,通过self.render_queue"""
        queue = self.render_queue
        if queue:
            e = (len(self.render_in_queue_items) + 1) / len(queue)
            return 100 - int(e * 100)
        return -1

    render_sum_progress: IntProperty(name='队列渲染进度',
                                     min=-1,
                                     max=100,
                                     get=get_sum_progress,
                                     set=set_prop,
                                     )


class RenderOut(GetProperty):

    @property
    def sub_pid_path(self) -> str:
        """反回子线程pid文件的路径"""
        return self._gen_tmp_path('thread_pid.txt')

    @property
    def write_pid(self) -> str:
        return f'''file =  open(r'{self.sub_pid_path}','w+');file.write(str(os.getpid()));file.close()'''

    @property
    def read_pid(self) -> int:
        """获取子进程的pid"""
        if not os.path.isfile(self.sub_pid_path):
            return -1

        with open(self.sub_pid_path, 'r') as file:
            line = file.read()
        return int(line)

    # path
    def _gen_tmp_path(self, path):
        """通过输入路径获取临时路径"""
        file_path = os.path.join(self.tmp_folder, path)
        return file_path

    @property
    def tmp_folder(self) -> str:
        """反回临时文件夹
        如果不存在则新建"""
        tmp = os.environ['TMP']
        folder = os.path.join(tmp, 'blender')

        if not os.path.isdir(folder):
            os.mkdir(folder)
        return folder

    @property
    def render_stdout_path(self) -> str:
        """反回渲染进度文件的路径"""
        return self._gen_tmp_path('render_progress.txt')

    # property
    @property
    def render_cmd(self) -> str:
        """ 渲染参数,将所有的代码缩至一行
        mkdir "{self.tmp_folder}" > NUL 2>&1"""
        cmd = ''

        render_parameter = [
            f'"{os.path.abspath(bpy.app.binary_path)}"',  # 打开blender
            '-b',  # 后台启动
            f'"{self.render_blend_file}"',  # 打开blend文件
            '--factory-startup',  # 出厂启动,不加载其它脚本

            self.render_out_path,  # 渲染输出路径设置

            self.render_python_expr,  # 渲染py表达式
            self.render_option,  # 渲染设置
            self.set_cycle_device,  # 设置cy的渲染设置(避免gpu无法使用)
        ]

        for i in render_parameter:
            if i:
                cmd += i + ' '
        return cmd

    @property
    def render_threads(self) -> str:
        """设置渲染线程"""
        if self.pref.use_threads:
            return f'--threads {bpy.context.scene.render.threads}'

    @property
    def render_out_path(self) -> str:
        """反回渲染输出路径
        TODO 输入到指定位置
        """
        return f'-o //render/{self.asset_name}####{bpy.context.scene.render.file_extension}'

    @property
    def set_cycle_device(self) -> str:
        """设置cy渲染器设备"""
        if self.pref.use_engine and bpy.context.scene.cycles.device == 'GPU':
            return f'-- --cycles-device {bpy.context.preferences.addons["cycles"].preferences.compute_device_type}'

    @property
    def render_option(self) -> str:
        """反回渲染设置
        -P {a}
        """
        return f'-f {bpy.context.scene.frame_current}'

    @property
    def render_python_expr(self) -> str:
        """渲染python 表达式
        用于设置渲染参数"""
        from .. import site_packages

        expr = [
            'import bpy,os,sys',
            f'sys.path.append(r\'{os.path.abspath(site_packages)}\')',
            # f'sys.path.append(bpy.context.preferences.filepaths.script_directory)',
            self.render_compositors,
            self.write_pid,
        ]
        expr.extend(self.render_parameter)
        py = f'''--python-expr "{';'.join(i for i in expr)}"'''

        for i in py.split(';'):
            log.debug(i)
        return py

    @property
    def render_compositors(self) -> 'str':
        """合成器特殊处理"""
        if not self.pref.use_compositors_node:
            return 'bpy.context.scene.use_nodes= bpy.context.scene.render.use_compositing = False'
        return 'print(\'use_compositors_node\')'

    @property
    def render_parameter(self) -> list:
        """反回渲染参数
        通过当前场景设置渲染的参数
        """
        ret = []

        pref = self.pref
        scene_str = 'bpy.context.scene'
        render_str = f'{scene_str}.render'
        cycle_str = f'{scene_str}.cycles'
        eve_str = f'{scene_str}.eevee'
        view_str = f'{scene_str}.view_settings'
        display_str = f'{scene_str}.display_settings'

        data = [
            [pref.use_format, {f'{render_str}.pixel_aspect_x',

                               f'{render_str}.pixel_aspect_y',
                               f'{render_str}.use_border',
                               f'{render_str}.use_crop_to_border',
                               f'{render_str}.fps',
                               f'{render_str}.fps_base',
                               }],

            [pref.use_format and pref.use_resolution_percentage, {
                f'{render_str}.resolution_percentage', }],
            [pref.use_format and pref.use_scene_resolution, {f'{render_str}.resolution_x',
                                                             f'{render_str}.resolution_y', }],
            [pref.use_samples, {f'{cycle_str}.use_adaptive_sampling',
                                f'{cycle_str}.adaptive_threshold',
                                f'{cycle_str}.samples',
                                f'{cycle_str}.adaptive_min_samples',
                                f'{cycle_str}.time_limit',

                                f'{eve_str}.taa_samples',
                                }],

            [pref.use_engine, {f'{render_str}.engine',
                               f'{cycle_str}.feature_set',
                               f'{cycle_str}.device',
                               f'{cycle_str}.shading_system',
                               }],

            [pref.use_film, {f'{render_str}.film_transparent',
                             f'{cycle_str}.film_transparent_glass',
                             f'{cycle_str}.film_transparent_roughness',
                             }],
            [pref.use_color_management, {f'{display_str}.display_device',

                                         f'{view_str}.view_transform',
                                         f'{view_str}.look',
                                         f'{view_str}.exposure',
                                         f'{view_str}.gamma',

                                         f'{scene_str}.sequencer_colorspace_settings.name',
                                         }],
            [pref.use_noise, {f'{eve_str}.taa_render_samples',
                              f'{eve_str}.taa_samples',
                              f'{eve_str}.use_taa_reprojection',

                              f'{cycle_str}.denoiser',
                              f'{cycle_str}.denoising_input_passes',
                              f'{cycle_str}.denoising_prefilter',
                              }],
            [pref.use_out, {f'{render_str}.use_file_extension',
                            f'{render_str}.use_render_cache',
                            f'{render_str}.use_overwrite',
                            f'{render_str}.use_placeholder',
                            }],
            [pref.use_performance, {f'{render_str}.threads_mode',
                                    f'{render_str}.threads',
                                    f'{cycle_str}.use_auto_tile',
                                    f'{cycle_str}.tile_size'}],

            [pref.use_compositors_node, {}],
            [pref.use_threads, {}],
            [pref.use_parameter, {}],
        ]

        def get_str(path: str):
            _property = from_path(path)
            if _property is not None:
                if isinstance(_property, str):
                    return path + f''' = \'{_property}\''''
                else:
                    return path + f' = {_property}'
            return f'print(\'get property error\',{path})'

        for (boo, items) in data:

            if boo:
                for item in items:
                    be = get_str(item)
                    ret.append(be)
        return ret


class BackgroundRender(RenderOut):
    """设置后台渲染参数"""
    background_render = {}  # 子线程{'thread':emm,'subprocess':emm}

    is_run_render = False

    @staticmethod
    def _kill_subprocess(pid: int) -> None:
        """输入pid号杀进程"""

        try:
            os.kill(pid, 9)
            log.debug(f'已结束进程   {pid}')
        except Exception as e:
            log.debug(f"'没有此进程', {pid}, {e.args}")

    def render(self) -> None:
        """渲染当前实例
        子线渲染
        """

        log.debug(f'queue render background {self}')

        cmd = self.render_cmd
        log.info(f'start render\t{cmd}')

        # return
        if self.render_error_count > 3:
            log.info(f'渲染重试次数超过最大值 {self} 3')
            self.render_state = 'RENDER_ERROR'
            return

        if not self.check_file:  # 检查文件
            self.render_state = 'FILE_ERROR'
            return
        self.background_render[self] = subprocess.Popen(cmd,
                                                        shell=True, text=True,
                                                        stdout=open(
                                                            self.render_stdout_path, "w+"),
                                                        bufsize=-1)

        self.render_state = 'IN_RENDER'  # 正在渲染中

    def stop_render(self):
        """停止渲染,将正在渲染的子进程直接杀掉
        """

        pid = self.subprocess_pid
        BackgroundRender._kill_subprocess(pid)
        BackgroundRender._kill_subprocess(self.read_pid)
        self.render_state = 'NOT_RENDER'

    @property
    def read_render_progress_strings(self) -> 'list[str]':
        """从临时文件 读取渲染进度字符串"""
        path = self.render_stdout_path
        with open(path, 'r') as file:
            return file.readlines()

    @staticmethod
    def from_set_get_cn_progress(string: str) -> str:
        """从输入的文字转换为中文的进度"""
        cn_data = {
            'Fra': '帧',
            'Peak': '峰值',
            'Time': '渲染时间',
            'ViewLayer': '视图层',
            'Synchronizing': '同步',
            'object': '物体',
            'Sample': '采样',
            'Finished': '完成',
            'Saved': '保存文件',
            'Loading': '加载',
            'Remaining': '预计渲染完成时间',
            'Rendered': '已渲染完',
            'Tiles': '拼贴',
            'Updating Images': '更新图像',
            'Waiting for render to start': '正在等待渲染开始 ',
            'Loading render kernels (may take a few minutes the first time)': '加载渲染内核(第一次可能需要几分钟)',
        }
        for (key, t) in cn_data.items():
            if key in string:
                string = string.replace(key, t)
        return string

    @property
    def read_render_progress(self) -> 'str':
        """渲染进度的最后一行"""
        if self.read_render_progress_strings:
            return self.read_render_progress_strings[-1]

    @property
    def process(self) -> 'subprocess.Popen':
        """反回子进程"""
        if self in self.background_render:
            return self.background_render[self]

    @property
    def in_render(self) -> bool:
        """正在渲染中的布尔值
        TODO import psutil 模块可能会不见
        """
        try:
            import psutil
            pid = self.subprocess_pid
        except Exception as e:
            log.debug(e.args)
            return True
        return pid and (pid in psutil.pids())

    @property
    def render_finished(self) -> bool:
        """渲染完成的布尔值"""
        last = self.read_render_progress
        is_render_finished = (last and ('Blender quit' in last))

        if is_render_finished:
            log.info(f'render finished {self}')

        return is_render_finished

    @property
    def subprocess_pid(self) -> int:
        """反回子进程pid"""
        proc = self.process
        if proc:
            return proc.pid

    def from_progress_read_render_file_path(self):
        """从渲染进度读取渲染完成文件路径
        """
        comp = re.compile(r"Saved: '(.+)'")

        for string in self.read_render_progress_strings[::-1]:
            fin = re.findall(comp, string)
            if len(fin):
                self.rendered_image_file_path = fin[0]
                return

    def update_render_progress(self):
        """更新渲染进度
        判断是否渲染完成(通过子进程是否还在运行来判断是否还在渲染)
        如果没有pid了则渲染错误
        如果最后是blender quit 则是渲染完成
        """
        if self.render_finished:
            self.from_progress_read_render_file_path()
            self.render_state = 'RENDERED'
        elif not self.in_render:
            self.render_state = 'RENDER_ERROR'
            self.render_error_count += 1


class RenderDraw(BackgroundRender):

    @property
    def state_text(self) -> str:
        """从状态获取文字
        绘制时使用
        """
        for i, t, d in self.render_state_items:
            if i == self.render_state:
                return t

    @staticmethod
    def switch_button(layout: 'bpy.types.UILayout', **kwargs):
        """绘制切换按钮"""

        from ..ops.queue import RenderStateSwitch
        layout.operator(RenderStateSwitch.bl_idname,
                        **kwargs)

    def draw_render_state(self, layout: 'bpy.types.UILayout'):
        """绘制渲染状态
        如果正在渲染则显示渲染进度
        """
        if self.is_in_render:
            # 正在渲染项
            if self.render_progress == -1:
                self.switch_button(layout, text='加载渲染资源..', icon='PAUSE')
                return
            layout.prop(self, 'render_progress', slider=True, text='')
            self.switch_button(layout, text='', icon='PAUSE')
        else:
            # 没渲染项
            self.switch_button(layout, text=self.state_text)
            self.draw_open_file(layout)

    def draw(self, layout: "bpy.types.UILayout", ui_list: 'bpy.types.UIList'):
        """绘制项
        在UIList里面直接调用此方法
        """
        layout.context_pointer_set('render_queue', self)
        sp = layout.split(factor=ui_list.split_factor, align=True)
        sp.alert = not self.valid
        self.draw_left(sp, ui_list)
        self.draw_right(sp, ui_list)

    def draw_left(self, layout: "bpy.types.UILayout", ui_list: 'bpy.types.UIList'):
        """绘制左边项"""
        row = layout.row(align=False)
        row.emboss = 'NONE_OR_STATUS'

        ro = row.row(align=False)
        ro.scale_x = 0.35
        ro.emboss = 'NORMAL'
        ro.prop(self, 'state_color', text='')
        from ..ui.asset_layout import icon_two
        icon = icon_two(self.selected, style='RESTRICT_SELECT')
        row.prop(self, 'selected', icon=icon, text='', )

        if self.render_blend_file and self.valid:
            row.label(text=self.asset_name)
            if ui_list.show_file_path:
                row.label(text=self.render_blend_file)
        else:
            row.prop(self, 'render_blend_file', text='')

    def draw_right(self, layout: "bpy.types.UILayout", ui_list: 'bpy.types.UIList'):
        """绘制右边项"""
        ro = layout.row(align=True)
        if ui_list.show_file_size:
            ro.label(text=f'{round(self.blend_size, 2)} MB')

        self.draw_render_state(ro)

        self.draw_remove_item(ro)

    def draw_menu(self, layout: "bpy.types.UILayout"):
        """绘制菜单"""

    def draw_remove_item(self, layout: "bpy.types.UILayout"):
        """绘制删除项"""
        from ..ops.queue import RenderQueueRemove
        layout.operator(RenderQueueRemove.bl_idname,
                        text='',
                        icon='PANEL_CLOSE',
                        ).queue_name = self.name

    def draw_open_file(self, layout: "bpy.types.UILayout"):
        """绘制打开文件"""
        layout.operator('wm.path_open',
                        text='',
                        icon='FILE_BLEND',
                        ).filepath = self.render_blend_file


class RenderItem(RenderDraw,
                 ):
    """渲染单个项
    单个项作为渲染文件
    打开文件夹或是文件来添加到渲染队列
    """

    # selected

    def set_not_render(self) -> None:
        """设置为未渲染项"""
        self.render_state = 'NOT_RENDER'

    def remove(self) -> None:
        """删除本项
        自己删自己
        """
        try:
            items = self.pref.render_items
            self.stop_render()
            items.remove(items.values().index(self))
        except Exception as e:
            log.error(f"remove error {self} {e.args}")


def register():
    bpy.utils.register_class(RenderItem)


def unregister():
    bpy.utils.unregister_class(RenderItem)
